package main

import (
	"bytes"
	"flag"
	"fmt"
	"gitee.com/LosingBattle/universe-builder/core/generate"
	"gitee.com/LosingBattle/universe-builder/core/util"
	"go/ast"
	"go/printer"
	"go/token"
	"go/types"
	"golang.org/x/tools/go/packages"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"text/template"
)

var (
	typeNames = flag.String("type", "", "comma-separated list of type; must be set [default:struct]")
	output    = flag.String("output", "", "output file name; default srcdir/<type>_builder.go")
)

// Usage is a replacement usage function for the flags package.
func Usage() {
	fmt.Fprintf(os.Stderr, "Usage of universe-builder:\n")
	fmt.Fprintf(os.Stderr, "\tuniverse-builder [flags] -type T [directory]\n")
	fmt.Fprintf(os.Stderr, "\tuniverse-builder [flags] -type T files... # Must be a single package\n")
	fmt.Fprintf(os.Stderr, "For more information, see:\n")
	fmt.Fprintf(os.Stderr, "\thttps://gitee.com/LosingBattle/universe-builder.git\n")
	fmt.Fprintf(os.Stderr, "Flags:\n")
	flag.PrintDefaults()
}

func main() {
	log.SetFlags(0)
	log.SetPrefix("universe-builder: ")
	flag.Usage = Usage
	flag.Parse()

	if len(*typeNames) == 0 {
		flag.Usage()
		os.Exit(2)
	}
	ts := strings.Split(*typeNames, ",")

	// We accept either one directory or a list of files. Which do we have?
	args := flag.Args()
	if len(args) == 0 {
		// Default: process whole package in current directory.
		args = []string{"."}
	}

	// Parse the package once.
	var dir string
	g := Generator{
		buf:      bytes.NewBuffer([]byte{}),
		walkMark: make(map[string]bool),
	}
	if len(args) == 1 && isDirectory(args[0]) {
		dir = args[0]
	} else {
		dir = filepath.Dir(args[0])
	}

	g.parsePackage(args)

	for _, typeName := range ts {
		g.generate(typeName)
	}

	outputName := *output
	if outputName == "" {
		baseName := fmt.Sprintf("%s_builder.go", util.SmallCamel(g.fileName))
		outputName = filepath.Join(dir, baseName)
	}

	buf := g.buf

	var src = (buf).Bytes()
	err := os.WriteFile(outputName, src, 0644)
	if err != nil {
		log.Fatalf("writing output: %s", err)
	}

	_, _ = exec.Command("gofmt", "-l", "-w", dir).Output()
}

// isDirectory reports whether the named file is a directory.
func isDirectory(name string) bool {
	info, err := os.Stat(name)
	if err != nil {
		log.Fatal(err)
	}
	return info.IsDir()
}

// Generator holds the state of the analysis. Primarily used to buffer
// the output for format.Source.
type Generator struct {
	buf        *bytes.Buffer // Accumulated output.
	pkg        *Package      // Package we are scanning.
	structInfo map[string]StructFieldInfoArr
	walkMark   map[string]bool
	fileName   string
}

func (g *Generator) Printf(format string, args ...interface{}) {
	buf := g.buf
	if g.buf == nil {
		buf = bytes.NewBufferString("")
		g.buf = buf
	}

	fmt.Fprintf(buf, format, args...)
}

// File holds a single parsed file and associated data.
type File struct {
	pkg     *Package  // Package to which this file belongs.
	file    *ast.File // Parsed AST.
	fileSet *token.FileSet
	// These fields are reset for each type being generated.
	typeName string // Name of the constant type.

}

type Package struct {
	name  string
	defs  map[*ast.Ident]types.Object
	files []*File
}

// parsePackage analyzes the single package constructed from the patterns and tags.
// parsePackage exits if there is an error.
func (g *Generator) parsePackage(patterns []string) {
	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedFiles |
			packages.NeedCompiledGoFiles | packages.NeedImports |
			packages.NeedTypes | packages.NeedTypesSizes |
			packages.NeedSyntax | packages.NeedTypesInfo,
		Tests: false,
	}
	pkgs, err := packages.Load(cfg, patterns...)
	if err != nil {
		log.Fatal(err)
	}
	if len(pkgs) != 1 {
		log.Fatalf("error: %d packages found", len(pkgs))
	}
	filepath.Base(pkgs[0].GoFiles[0])

	filenameWithSuffix := filepath.Base(pkgs[0].GoFiles[0])
	fileSuffix := path.Ext(filenameWithSuffix)

	g.fileName = strings.TrimSuffix(filenameWithSuffix, fileSuffix)
	g.addPackage(pkgs[0])
}

// addPackage adds a type checked Package and its syntax files to the generator.
func (g *Generator) addPackage(pkg *packages.Package) {

	g.pkg = &Package{
		name:  pkg.Name,
		defs:  pkg.TypesInfo.Defs,
		files: make([]*File, len(pkg.Syntax)),
	}
	for i, file := range pkg.Syntax {
		g.pkg.files[i] = &File{
			file:    file,
			pkg:     g.pkg,
			fileSet: pkg.Fset,
		}
	}
}

// generate produces the String method for the named type.
func (g *Generator) generate(typeName string) {

	g.Printf("package %s\n", g.pkg.name)
	g.Printf("\n")

	for _, file := range g.pkg.files { //按包来的，读取包下的所有文件

		file.typeName = typeName
		//ast.Print(file.fileSet, file.file)
		if file.file != nil {

			structInfo, err := ParseStruct(file.file, file.fileSet)
			if err != nil {
				fmt.Println("失败:" + err.Error())
				return
			}

			for stName, info := range structInfo {
				if stName != typeName {
					continue
				}
				typeBuilderName := fmt.Sprintf("%sBuilder", util.BigCamel(stName))
				//生成builder
				g.Printf("%s\n", genBuilder(info, typeBuilderName))
				for _, field := range info {

					if strings.Contains(field.Name, "Flag") {
						continue
					}
					g.Printf("%s\n", genSetter(typeBuilderName, field.Name, field.Type))
				}

				g.Printf("%s\n", genBuild(util.BigCamel(stName), typeBuilderName, info))
			}

		}
	}

}

type StructFieldInfo struct {
	Name      string
	Type      string
	Note      string
	IsPointer bool
}

func (s *StructFieldInfo) Generate() string {
	var p generate.PrintAtom

	n := util.SmallCamel(s.Name)

	if s.Note != "" {
		p.Add(n, s.Type, fmt.Sprintf("//%s", s.Note))
	} else {
		p.Add(n, s.Type)
	}

	strOut := ""
	for _, v := range p.Generates() {
		strOut += v
	}

	return strOut
}

type StructFieldInfoArr []StructFieldInfo

func ParseStruct(file *ast.File, fileSet *token.FileSet) (structMap map[string]StructFieldInfoArr, err error) {
	structMap = make(map[string]StructFieldInfoArr)

	collectStructs := func(x ast.Node) bool {
		ts, ok := x.(*ast.TypeSpec)
		if !ok || ts.Type == nil {
			return true
		}

		// 获取结构体名称
		structName := ts.Name.Name

		s, ok := ts.Type.(*ast.StructType)
		if !ok {
			return true
		}
		fileInfos := make([]StructFieldInfo, 0)
		for _, field := range s.Fields.List {

			name := field.Names[0].Name
			//type关键字
			if name == "Type" {
				name = "Ty"
			}
			info := StructFieldInfo{Name: name}
			var typeNameBuf bytes.Buffer
			err := printer.Fprint(&typeNameBuf, fileSet, field.Type)
			if err != nil {
				fmt.Println("获取类型失败:", err)
				return true
			}

			if util.IsFirstLower(name) {
				continue
			}

			t := typeNameBuf.String()

			//指针类型
			if strings.HasPrefix(t, "*") {
				t = t[1:]
				info.IsPointer = true
			}

			info.Type = t
			info.Note = strings.Replace(field.Comment.Text(), "\n", "", -1)
			fileInfos = append(fileInfos, info)
			infoFlag := StructFieldInfo{Name: fmt.Sprintf("%sFlag", name), Type: "bool"}
			fileInfos = append(fileInfos, infoFlag)

		}
		structMap[structName] = fileInfos
		return false
	}

	ast.Inspect(file, collectStructs)

	return structMap, nil
}

func genBuilder(info StructFieldInfoArr, typeBuilderName string) string {

	var pa generate.PrintAtom

	pa.Add("type", typeBuilderName, "struct {")
	for _, i2 := range info {
		pa.Add(i2.Generate())
	}
	pa.Add("}")

	tpl := `func New{{.Struct}}() *{{.Struct}} {
			return &{{.Struct}}{}
		}`
	t := template.New("builder")

	t = template.Must(t.Parse(tpl))
	res := bytes.NewBufferString("")
	t.Execute(res, map[string]string{
		"Struct": typeBuilderName,
	})

	pa.Add(res.String())

	strOut := ""
	for _, v := range pa.Generates() {
		strOut += v + "\n"
	}

	return strOut
}

func genBuild(structName string, typeBuilderName string, info StructFieldInfoArr) string {
	var pa generate.PrintAtom
	pa.Add("func( builder *", typeBuilderName, ") Build()", "*", structName, "{")
	pa.Add("req := &", structName, "{}")
	for _, field := range info {

		if strings.Contains(field.Name, "Flag") {
			continue
		}

		sn := util.SmallCamel(field.Name)
		pa.Add(fmt.Sprintf("if builder.%sFlag {", sn))
		if field.IsPointer {
			pa.Add(fmt.Sprintf("req.%s", field.Name), " = ", fmt.Sprintf("&builder.%s", sn))
		} else {
			pa.Add(fmt.Sprintf("req.%s", field.Name), " = ", fmt.Sprintf("builder.%s", sn))
		}

		pa.Add("}")
	}
	pa.Add("return req")

	pa.Add("}")

	strOut := ""
	for _, v := range pa.Generates() {
		strOut += v + "\n"
	}
	return strOut
}

func genSetter(structName, fieldName, typeName string) string {
	tpl := `func (builder *{{.Struct}}) {{.Field}}({{.Field_2}} {{.Type}}) *{{.Struct}} {
	builder.{{.Field_2}} = {{.Field_2}}
	builder.{{.Field_2}}Flag = true
	return builder
}`
	t := template.New("setter")
	t = template.Must(t.Parse(tpl))
	res := bytes.NewBufferString("")
	t.Execute(res, map[string]string{
		"Struct":  structName,
		"Field":   fieldName,
		"Field_2": util.SmallCamel(fieldName),
		"Type":    typeName,
	})
	return res.String()
}
